{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 합성 테스트 데이터셋 생성\n",
    "\n",
    "**왜 합성 테스트 데이터(Synthetic Test Dataset) 인가?**\n",
    "\n",
    "RAG(검색 증강 생성) 증강 파이프라인의 성능을 평가하는 것은 매우 중요합니다. \n",
    "\n",
    "그러나 문서에서 수백 개의 QA(질문-문맥-응답) 샘플을 수동으로 생성하는 것은 시간과 노동력이 많이 소요될 수 있습니다. 또한 사람이 만든 질문은 철저한 평가에 필요한 복잡성 수준에 도달하기 어려워 궁극적으로 평가의 품질에 영향을 미칠 수 있습니다. \n",
    "\n",
    "합성 데이터 생성을 사용하면 데이터 집계 프로세스에서 **개발자의 시간을 90%** 까지 줄일 수 있습니다.\n",
    "\n",
    "- RAGAS: https://docs.ragas.io/en/latest/concepts/testset_generation.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "아래의 주석을 해제한 후 실행하여 패키지를 설치 후 진행해주세요"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**참고**\n",
    "\n",
    "(2024.10.08 현재) ragas 패키지는 0.2.16 버전의 langchain 패키지와 호환됩니다.\n",
    "\n",
    "따라서 버전을 0.2.16 버전으로 설치해야 합니다. (현재, 0.3.x 버전으로 우리가 사용하는 버전이 더 상위 버전입니다)\n",
    "\n",
    "아래의 주석을 해제 후 진행해 주세요!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -q langchain==0.2.16\n",
    "!pip install -q ragas==0.1.19"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import langchain\n",
    "import ragas\n",
    "\n",
    "print(f\"LangChain Version: {langchain.__version__}\")\n",
    "print(f\"Ragas Version: {ragas.__version__}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH16-Evaluations\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 실습에 활용한 문서\n",
    "\n",
    "소프트웨어정책연구소(SPRi) - 2023년 12월호\n",
    "\n",
    "- 저자: 유재흥(AI정책연구실 책임연구원), 이지수(AI정책연구실 위촉연구원)\n",
    "- 링크: https://spri.kr/posts/view/23669\n",
    "- 파일명: `SPRI_AI_Brief_2023년12월호_F.pdf`\n",
    "\n",
    "_실습을 위해 다운로드 받은 파일을 `data` 폴더로 복사해 주시기 바랍니다_\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 문서 전처리"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "문서를 로드 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PDFPlumberLoader\n",
    "\n",
    "# 문서 로더 생성\n",
    "loader = PDFPlumberLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "\n",
    "# 문서 로딩\n",
    "docs = loader.load()\n",
    "\n",
    "# 목차, 끝 페이지 제외\n",
    "docs = docs[3:-1]\n",
    "\n",
    "# 문서의 페이지수\n",
    "len(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "각 문서 객체에는 `metadata` 를 통해 액세스할 수 있는 문서에 대한 추가 정보를 저장하는 데 사용할 수 있는 메타데이터 사전이 포함되어 있습니다. \n",
    "\n",
    "메타데이터 사전에는 `filename` 이라는 키가 포함되어 있는지 확인하세요. \n",
    "\n",
    "이 키는 Test datasets 생성 프로세스에서 활용될 것이므로. 메타데이터의 `filename` 속성은 동일한 문서에 속한 청크를 식별하는 데 사용됩니다. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "docs[0].metadata"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# metadata 설정(filename 이 존재해야 함)\n",
    "for doc in docs:\n",
    "    doc.metadata[\"filename\"] = doc.metadata[\"source\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 데이터셋 생성"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from ragas.testset.generator import TestsetGenerator\n",
    "from ragas.testset.evolutions import simple, reasoning, multi_context, conditional\n",
    "from ragas.llms import LangchainLLMWrapper\n",
    "from ragas.embeddings import LangchainEmbeddingsWrapper\n",
    "from ragas.testset.extractor import KeyphraseExtractor\n",
    "from ragas.testset.docstore import InMemoryDocumentStore\n",
    "\n",
    "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "\n",
    "# 데이터셋 생성기\n",
    "generator_llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "# 데이터셋 비평기\n",
    "critic_llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "# 문서 임베딩\n",
    "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "DocumentStore를 초기화합니다. 사용자 정의 LLM과 임베딩을 사용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 텍스트 분할기를 설정합니다.\n",
    "splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)\n",
    "\n",
    "# LangChain의 ChatOpenAI 모델을 LangchainLLMWrapper로 감싸 Ragas와 호환되게 만듭니다.\n",
    "langchain_llm = LangchainLLMWrapper(ChatOpenAI(model=\"gpt-4o-mini\"))\n",
    "\n",
    "# 주요 구문 추출기를 초기화합니다. 위에서 정의한 LLM을 사용합니다.\n",
    "keyphrase_extractor = KeyphraseExtractor(llm=langchain_llm)\n",
    "\n",
    "# ragas_embeddings 생성\n",
    "ragas_embeddings = LangchainEmbeddingsWrapper(embeddings)\n",
    "\n",
    "# InMemoryDocumentStore를 초기화합니다.\n",
    "# 이는 문서를 메모리에 저장하고 관리하는 저장소입니다.\n",
    "docstore = InMemoryDocumentStore(\n",
    "    splitter=splitter,\n",
    "    embeddings=ragas_embeddings,\n",
    "    extractor=keyphrase_extractor,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "TestSet 을 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "generator = TestsetGenerator.from_langchain(\n",
    "    generator_llm,\n",
    "    critic_llm,\n",
    "    ragas_embeddings,\n",
    "    docstore=docstore,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**질문의 유형별 분포**\n",
    "\n",
    "- simple: 간단한 질문\n",
    "- reasoning: 추론이 필요한 질문\n",
    "- multi_context: 여러 맥락을 고려해야 하는 질문\n",
    "- conditional: 조건부 질문"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문 유형별 분포 결정\n",
    "# simple: 간단한 질문, reasoning: 추론이 필요한 질문, multi_context: 여러 맥락을 고려해야 하는 질문, conditional: 조건부 질문\n",
    "distributions = {simple: 0.4, reasoning: 0.2, multi_context: 0.2, conditional: 0.2}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- documents: 문서 데이터\n",
    "- test_size: 생성할 질문의 수\n",
    "- distributions: 질문 유형별 분포\n",
    "- with_debugging_logs: 디버깅 로그 출력 여부"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 테스트셋 생성\n",
    "# docs: 문서 데이터, 10: 생성할 질문의 수, distributions: 질문 유형별 분포, with_debugging_logs: 디버깅 로그 출력 여부\n",
    "testset = generator.generate_with_langchain_docs(\n",
    "    documents=docs,\n",
    "    test_size=10,\n",
    "    distributions=distributions,\n",
    "    with_debugging_logs=True,\n",
    "    raise_exceptions=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 생성된 테스트셋을 pandas DataFrame으로 변환\n",
    "test_df = testset.to_pandas()\n",
    "test_df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "DataFrame 에 저장된 데이터셋을 csv 파일로 저장합니다"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DataFrame의 상위 5개 행 출력\n",
    "test_df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DataFrame을 CSV 파일로 저장\n",
    "test_df.to_csv(\"data/ragas_synthetic_dataset.csv\", index=False)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
